Skip to main content

TypeScript 集成

TypeScript 是使用 CLI 构建集成的首选语言。

Zapier CLI 和平台的 v17 版本发布后,对 TypeScript 集成的支持得到了显著扩展和改进。本文档详细介绍了如何创建、使用和测试 TypeScript 集成。

入门指南

通过 zapier init 生成的 TypeScript+ESM 模板提供了启动 TypeScript 集成所需的所有配置。如果您要在现有应用中添加 TypeScript 支持,请查看下面的 TypeScript 集成结构 部分,以获取更多关于编译器设置、使用 zapier-platform-core 库,以及如何用 TypeScript 组合触发器、创建操作和搜索操作的详细信息。

$ zapier init my-app --template typescript+esm
$ cd my-app
$ npm install

这将在 ./my-app 目录下创建一个新应用,并安装依赖项,应用结构如下:

my-app/
├── src/
│ ├── authentication.ts
│ ├── middleware.ts
│ ├── index.ts
│ ├── triggers/
│ ├── creates/
│ └── searches/
├── package.json
└── tsconfig.json

与 JS 集成的差异

TypeScript 集成与 JavaScript 集成的实现存在一些关键差异,值得注意。

  • define 辅助函数用于封装您的 App、触发器、创建操作、搜索操作以及输入字段定义。

  • 代码存储在 src/ 目录中,并编译到 dist/ 目录。集成的入口文件现变为 ./src/index.ts

  • 采用现代的 import / export 语法,而非 Node 的 require / module.exports 赋值。

  • 我们推荐使用 Vitest 进行测试,它是 Jest 的高效替代方案,提供更好的 ESM+TypeScript 支持。

输入字段

请使用 defineInputFields 辅助函数来定义触发器、创建操作和搜索操作的所有输入字段。该函数会自动推断指定输入字段的类型。

示例代码如下:

import { defineInputFields, defineXyz, // 例如 defineTrigger, defineCreate, defineSearch
type XyzPerform, // 根据操作类型不同
type InferInputData,
} from "zapier-platform-core";

const inputFields = defineInputFields([
// 输入字段定义在这里。
]);

const perform = (async (z, bundle) => {
// bundle.inputData 的类型将从 inputFields 推断。
// 在这里执行请求和应用逻辑。
}) satisfies XyzPerform<InferInputData>;

export default defineXyz({
// ... 其他细节:key, display, noun 等
operations: {
inputFields,
perform, // 在这里组合。
},
});

这与之前的做法不同,以前通常是将输入字段和执行函数定义在顶层导出的操作中。

从输入字段推断输入数据

InferInputData 类型可以从使用 defineInputFields 定义的输入字段中推断输入数据的结构。这一点非常有用,因为它为各种执行函数提供了 bundle.inputData 属性的准确类型。

import { defineInputFields, type InferInputData } from "zapier-platform-core";

const inputFields = defineInputFields([
{ key: "a", type: "number", required: true },
{ key: "b", type: "string", required: false },
]);

type InputData = InferInputData; // ^? { a: number; b?: string }

复用输入字段

输入字段经常在触发器、创建操作和搜索操作之间重复使用。为简化这一过程,还提供了单数形式的 defineInputField 辅助函数,用于定义单个输入字段。这些函数可以放置在 src/ 目录的适当位置,然后由需要的操作导入。

// ./src/inputFields.ts
import { defineInputField } from "zapier-platform-core";

export const SOME_COMMON_FIELD = defineInputField({
key: "someKey",
type: "boolean",
required: true,
});

// ./src/triggers/someTrigger.ts
import { defineInputFields, type InferInputData } from "zapier-platform-core";
import { SOME_COMMON_FIELD } from "../inputFields";

const inputFields = defineInputFields([
SOME_COMMON_FIELD,
{ key: "someOtherKey", type: "string", required: true },
]);

type InputData = InferInputData; // ^? { someKey: boolean; someOtherKey: string }

动态输入字段

输入字段可以是动态的,即它们是函数,在运行时执行并返回零个、一个或多个输入字段。这在输入字段依赖于其他输入字段的值时,或者输入字段定义是从您的 API 获取并准备时(如 CRM 或数据库 API),非常实用。

以下示例展示了一个动态输入字段,当先前的布尔输入字段设置为 true 时,会选择性地包含一个自定义主题字段。

import { defineInputField, defineInputFields, type InferInputData } from "zapier-platform-core";

const customSubjectField = defineInputField((z, bundle) => {
if (bundle.inputData.useCustomSubject as boolean) {
return defineInputFields([
{ key: "customSubject", type: "string", required: true },
]);
}
return defineInputFields([]); // 重要:所有返回必须有类型。
});

const inputFields = defineInputFields([
{ key: "useCustomSubject", type: "boolean", required: true },
customSubjectField,
]);

type InputData = InferInputData; // ^? { useCustomSubject: boolean; customSubject?: string }

在动态输入字段函数中,bundle.inputData 不会为其相邻输入字段提供类型信息。我们建议将引用的字段强制转换为其已知类型。例如上述代码中,使用了 inputData.useCustomSubject as boolean

已知字段与未知字段

上述示例显示了一个已知动态输入字段,其中键和类型是预先确定的,函数逻辑用于根据其他字段决定是否包含它。这些输入字段可以被类型系统捕获,并包含在 bundle.inputData 属性中。从输入函数返回的输入字段总是被视为可选的,即使其定义中指定了 required: true,因为无法保证它们一定存在。

当字段无法预先确定时,输入函数可以返回完全未知的输入字段。这在输入字段是从 API 返回的数据派生时特别有用。在这种情况下,已知的输入将被保留,但 bundle.inputData 属性会将任何其他属性视为 unknown

// 未知动态输入字段示例
import { defineInputField, defineInputFields, type InferInputData, type PlainInputField } from "zapier-platform-core";

/** 示例 API 字段类型,与 Zapier 字段不同 */
type ApiField = {
id: string;
label: string;
type: "Text" | "Number" | "Boolean";
};

/** 从 API 获取并准备多个输入字段。 */
const getItemFields = defineInputField(async (z, { inputData }) => {
const response = await z.request(`${API_URL}/item/${inputData.itemId}/fields`);
return response.data.map(({ id, label, type }) =>
defineInputField({
key: id,
label,
type: type.toLowerCase() as "text" | "number" | "boolean",
})
);
});

const inputFields = defineInputFields([
{ key: "itemId", type: "string", required: true, dynamic: "item.id.label" },
getItemFields,
]);

type InputData = InferInputData; // ^? { itemId: string; [x: string]: unknown; }

执行函数类型

现在,所有不同类型的执行操作都有专用的 type。这些类型应使用 type 限定导入。define 辅助函数中的相关 operation 部分会确保不同执行函数的正确类型。它们包括:

  • 轮询触发器:PollingTriggerPerform

  • Webhook 触发器:WebhookTriggerPerformWebhookTriggerPerformListWebhookTriggerPerformSubscribeWebhookTriggerPerformUnsubscribe

  • 创建操作:CreatePerformCreatePerformResume

  • 搜索操作:SearchPerformSearchPerformResume

这些类型都至少接受一个类型参数,即 bundle.inputData 属性的结构。使用 satisfies XyzPerform 来强制执行正确类型,同时保留返回类型信息。

import type { PollingTriggerPerform } from "zapier-platform-core";

const perform = (async (z, bundle) => {
// bundle.inputData 的类型为 { a: number; b?: string }
}) satisfies PollingTriggerPerform<InputData>;

在大多数情况下,输入数据是从输入字段派生的,因此可以使用 InferInputData 类型:

import type { PollingTriggerPerform, InferInputData } from "zapier-platform-core";

const perform = (async (z, bundle) => {
// bundle.inputData 的类型为 { a: number; b?: string }
}) satisfies PollingTriggerPerform<InferInputData>;

现在,bundle.inputData 属性将被正确类型化为从 inputFields 数组推断的输入数据。

TypeScript 集成结构

TypeScript 集成遵循与 JavaScript 集成相同的结构和 底层模式。关键区别在于,TypeScript 应用需要用相关 define 辅助函数包装集成的核心组件,以提供更深入的类型推断。这些组件包括:

  • defineApp() – 顶层应用的主体函数。

  • defineTrigger() / defineCreate() / defineSearch() – 用于集成中的相应操作。

  • defineInputFields() – 封装输入字段数组,以简化处理输入字段的完整类型信息。

src/index.ts

应用的入口点变为 src/index.ts。它应导入其依赖项,默认导出保持为 Application 对象。通过 defineApp 包装它有助于验证其结构。

否则,它是一个标准的集成,您可以像在 JavaScript 集成中一样注册 Auth、中间件、hydrators、触发器、创建操作和搜索操作!

import { defineApp, version as platformVersion } from "zapier-platform-core";
import packageJson from "../package.json" with { type: "json" };
import authentication from "./authentication";
import someCreate from "./creates/some-create";
import someSearch from "./searches/some-search";
import someTrigger from "./triggers/some-trigger";

export default defineApp({
// 重要:注意使用 `defineApp`
version: packageJson.version,
platformVersion,
authentication,
creates: {
[someCreate.key]: someCreate,
},
searches: {
[someSearch.key]: someSearch,
},
triggers: {
[someTrigger.key]: someTrigger,
},
});

src/authentication.ts

认证定义为一个标准对象,并从名为 src/authentication.ts 的文件中导出。它使用 satisfies Authentication 约束来验证其结构,而不使用 define 辅助函数。

// ./src/authentication.ts
import type { Authentication } from "zapier-platform-core";
import { SCOPES } from "./constants";

export default {
type: "oauth2",
test: {
url: "https://api.webflow.com/v2/token/authorized_by",
},
connectionLabel: "{{email}}",
oauth2Config: {
authorizeUrl: {
url: "https://webflow.com/oauth/authorize",
params: {
client_id: "{{process.env.CLIENT_ID}}",
response_type: "code",
scope: SCOPES.join(" "),
redirect_uri: "{{bundle.inputData.redirect_uri}}",
state: "{{bundle.inputData.state}}",
},
},
getAccessToken: {
url: "https://api.webflow.com/oauth/access_token",
method: "POST",
params: {
client_id: "{{process.env.CLIENT_ID}}",
client_secret: "{{process.env.CLIENT_SECRET}}",
code: "{{bundle.inputData.code}}",
grant_type: "authorization_code",
redirect_uri: "{{bundle.inputData.redirect_uri}}",
},
},
},
} satisfies Authentication; // 重要:注意使用 `satisfies`

src/middleware.ts

中间件函数是从 src/middleware.ts 导出的函数,类型为 BeforeRequestMiddlewareAfterResponseMiddleware。它们在 /src/index.ts 中以与 JavaScript 集成相同的方式注册。

// ./src/middleware.ts
import type { BeforeRequestMiddleware } from "zapier-platform-core";

export const addBearerHeader: BeforeRequestMiddleware = (request, z, bundle) => {
if (bundle.authData.access_token && !request.headers?.Authorization) {
request.headers = {
...request.headers,
Authorization: `Bearer ${bundle.authData.access_token}`,
};
}
return request;
};

src/triggers/pollingTrigger.ts

触发器、创建操作和搜索操作现在建议将它们的输入和执行函数定义为独立对象,并在文件的默认导出 define 辅助函数中组合。

// ./src/triggers/pollingTrigger.ts
import { defineInputFields, defineTrigger, type InferInputData, type PollingTriggerPerform } from "zapier-platform-core";
import { API_URL } from "../constants.js";

const inputFields = defineInputFields([
// 重要:注意 `defineInputFields`
{ key: "country", type: "string", required: false },
]);

const perform = (async (z, bundle) => {
// `bundle.inputData` 的类型为 `{ country?: string }`
const response = await z.request(`${API_URL}/movies`);
return response.data;
}) satisfies PollingTriggerPerform<InferInputData>; // 重要:注意 `satisfies`

export default defineTrigger({
key: "movie",
noun: "Movie",
display: {
label: "New Movie",
description: "当新电影创建时触发。",
},
operation: {
type: "polling",
inputFields,
perform,
sample: {
id: "1",
title: "example",
},
},
});

src/tsconfig.json

tsconfig.json 文件用于配置 TypeScript 编译器。以下是针对开发 TypeScript 集成推荐的设置。

{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"esModuleInterop": true,
"noUncheckedIndexedAccess": true,
"isolatedModules": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true
},
"include": ["./src/**/*.ts"]
}
---